Bwcaptcha: Another Implementation of Captcha
Jimmy Shiau, Engineer, Potix Corporation
January 5, 2010
Applicable to ZK 5.0 and later
Introduction
The bwcaptcha
component was originally created by Dennis Chen. I have made it compatible with ZK 5 recently. The API is similar to the standard captcha
component, such as specifying the color, size, font and so on. In this small talk, I'll show you how to use it and how it is implemented. You'll see how easy it is to create a BW captcha ZK component.
How to use the bwcaptcha component
Here is the bwcaptcha's attribute specification table:
Attribute | Usage | Default Value |
---|---|---|
width | Sets the width of the captcha | "135" (unit is pixel) |
height | Sets the height of the captcha | "55" (unit is pixel) |
bgColor | Sets the background color of the captcha | 0x8888FF (in 0xRRGGBB) |
fontColor | Sets the font color of the captcha | 0xFF6666 (in 0xRRGGBB) |
fontBgColor | Sets the font background color of the captcha | 0xFFDDFF (in 0xRRGGBB) |
lineColor | Sets the line color of the captcha | 0x7777DD (in 0xRRGGBB) |
thickness | Set thickness of line | 3 |
font | Sets the font of the captcha | new Font("Courier", Font.BOLD, 32) |
length | Sets length of the auto generated text value | 5 |
Create the bwcaptcha component in your zul file
You can use a <bw.captcha/>
tag to create a bwcaptcha
component, and call the getValue
method to retrieve the value of the captcha object and compare it with the user input. You can choose to ignore case sensitivity by using the equalsIgnoreCase
method.
<zscript><![CDATA[
import org.zkforge.bwcaptcha.Captcha;
void verifyCaptcha(Textbox tbox,Captcha capt){
if(!capt.getValue().equals(tbox.getValue())){ throw new WrongValueException(tbox,"Code Error!"); }
}
void verifyCaptchaIgnoreCase(Textbox tbox,Captcha capt){
if(!capt.getValue().equalsIgnoreCase(tbox.getValue())){ throw new WrongValueException(tbox,"Code Error!"); }
}
]]></zscript>
<vbox>
<button label="re-generate" onClick="cap1.randomValue();"/>
<bw.captcha id="cap1" />
<textbox onChange="verifyCaptcha(self,cap1)" />
<label value="ignore case"/>
<textbox onChange="verifyCaptchaIgnoreCase(self,cap1)" />
</vbox>
Here is a quick demo:
Specify how the random value will be generated
Specify the char array to be used for generating a random captcha value and also enter the desired length for the random value.
<zscript><![CDATA[
char[] captchars = {'0','1','2','3','4','5','6','7','8','9'};
]]></zscript>
<bw.captcha id="cap3" length="6" captchars="${captchars}"/>
A quick demo:
Change style on the bwcaptcha component
You can create a Font
object for the bwcaptcha component so that styling elements, such as text font, font color, background color, and line thickness may be customized. Color is set in the format: 0xRRGGBB.
<zscript><![CDATA[
import java.awt.Font;
Font font = new Font("Tohama",Font.BOLD,40);
]]></zscript>
<bw.captcha id="cap4" width="200px" height="80px" thickness="7" font="${font}">
<attribute name="onCreate"><![CDATA[
cap4.setBgColor(0x666666);
cap4.setFontColor(0x00FF00);
cap4.setFontBgColor(0x55AA55);
cap4.setLineColor(0xFFFF00);
]]></attribute>
</bw.captcha>
A quick demo:
Implement your own algorithm for the bwcaptcha random value
You can implement your own algorithm for generating the bwcaptcha random value and then call self.value
(which is equivalent to self.value
in this context) to generate the random value.
<zscript><![CDATA[
String generate(){
/*
your implementation
*/
}
]]></zscript>
<button label="re-generate" onClick="cap5.value = generate()"/>
<bw.captcha id="cap5" onCreate='self.value = generate()'/>
A quick demo:
Behind the Scene: The Implementation
This component is an image which uses java Graphics2D to draw from automatically generated value.
Create a Graphics2D object
Create a BufferedImage
object and give it width and height. Create a Graphics2D
object by calling BufferedImage
object's createGraphics
method and set the background and color.
BufferedImage bi = new BufferedImage(_intWidth, _intHeight, BufferedImage.TYPE_BYTE_INDEXED);
Color bgC = new Color(_bgColor);
Color fontC = new Color(_fontColor);
Color lineC = new Color(_lineColor);
Graphics2D graphics = bi.createGraphics();
graphics.setBackground(bgC);
graphics.setColor(bgC);
graphics.fillRect(0, 0, bi.getWidth(), bi.getHeight());
Draw the text
First, draw a rectangle for the background. Draw the text twice, one for the foreground text and the other for the background text, then set color on both.
graphics.fillRect(0, 0, bi.getWidth(), bi.getHeight());
Font font = getFont();
TextLayout textTl = new TextLayout(captchaValue, font, new FontRenderContext(null, true, false));
float x = 10;
float y = (float)(_intHeight - (_intHeight/2 - font.getSize()/2));
graphics.setColor(new Color(_fontBgColor));
textTl.draw(graphics, x+7, y+7);
graphics.setColor(fontC);
textTl.draw(graphics, x, y);
Distort the Text
The generator object generates a period number in random, and redraw the graphic from top to down first ,then from left to right, making the text distorted in a wave-like fashion.
shear(graphics, bi.getWidth(), bi.getHeight(), bgC);
private void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
private void shearX(Graphics g, int w1, int h1, Color color) {
int period = generator.nextInt(10) + 5;
boolean borderGap = true;
int frames = 15;
int phase = generator.nextInt(5) + 2;
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (Math.PI*1* (double) phase) / (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
private void shearY(Graphics g, int w1, int h1, Color color) {
int period = generator.nextInt(30) + 10; // 50;
boolean borderGap = true;
int frames = 15;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (Math.PI*2 * (double) phase) / (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
Draw a thick line
We will give two random coordinates for the points on the left and right edges. The drawThickLine method will compute the points at the corners taken into account the thickness of line, and draw a polygon.
drawThickLine(graphics, 0, generator.nextInt(_intHeight) + 1, _intWidth, generator.nextInt(_intHeight) + 1, _thickness, lineC);
private void drawThickLine(Graphics g, int x1, int y1, int x2, int y2, int thickness, Color c) {
// The thick line is in fact a filled polygon
g.setColor(c);
int dX = x2 - x1;
int dY = y2 - y1;
// line length
double lineLength = Math.sqrt(dX * dX + dY * dY);
double scale = (double) (thickness) / (2 * lineLength);
// The x and y increments from an endpoint needed to create a rectangle...
double ddx = -scale * (double) dY;
double ddy = scale * (double) dX;
ddx += (ddx > 0) ? 0.5 : -0.5;
ddy += (ddy > 0) ? 0.5 : -0.5;
int dx = (int) ddx;
int dy = (int) ddy;
// Now we can compute the corner points...
int xPoints[] = new int[4];
int yPoints[] = new int[4];
xPoints[0] = x1 + dx;
yPoints[0] = y1 + dy;
xPoints[1] = x1 - dx;
yPoints[1] = y1 - dy;
xPoints[2] = x2 - dx;
yPoints[2] = y2 - dy;
xPoints[3] = x2 + dx;
yPoints[3] = y2 + dy;
g.fillPolygon(xPoints, yPoints, 4);
}
Generate an Image
Finally, we use CompressQualityParam
object to compress the image file, and return it as a byte array. We use this byte array to create an AImage
instance, because bwcaptcha
extends the zk Image
component, we'll just call setContent
to put the graphic in content.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
//ImageIO.write(bi,"JPEG", baos);
ImageOutputStream ios;
ios = ImageIO.createImageOutputStream(baos);
ImageWriter writer = null;
Iterator iter = ImageIO.getImageWritersByFormatName("jpg");
if (iter.hasNext()) {
writer = (ImageWriter)iter.next();
}
// Prepare output file
writer.setOutput(ios);
// Set the compression quality
CompressQualityParam iwparam = new CompressQualityParam();
iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT) ;
iwparam.setCompressionQuality(0.9F);
// Write the image
writer.write(null, new IIOImage(bi, null, null), iwparam);
// Cleanup
ios.flush();
writer.dispose();
ios.close();
baos.close();
} catch (IOException e) {
throw new RuntimeException(e.getMessage(),e);
}
return baos.toByteArray();
private class CompressQualityParam extends JPEGImageWriteParam {
public CompressQualityParam() {
super(Locale.getDefault());
}
public void setCompressionQuality(float quality) {
if (quality < 0.0F || quality > 1.0F) {
throw new IllegalArgumentException("Quality Out-Of-Bounds.");
}
this.compressionQuality = 256 - (quality * 256);
}
}
final AImage media = new AImage("captcha"+new Date().getTime(), bytes);
setContent(media);
Download
- Download bwcaptcha jar file.
- SVN: https://zkforge.svn.sourceforge.net/svnroot/zkforge/trunk/bwcaptcha
Summary
Users have full control of the bwcaptcha
component on the server side, without any implementation needed on the client side. For the client, simply extend ZK 5 Image
component, and implement how the graphic is to be drawn and then use Image's
API to set content.
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |